#!/usr/bin/python
#
# Copyright (c) 2010, Takashi Ito
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
# 3. Neither the name of the authors nor the names of its contributors
#    may be used to endorse or promote products derived from this software
#    without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

import sqlalchemy as sa
from sqlalchemy.orm import mapper, relation, sessionmaker, backref
from tracext.sa import session
from trac.web.chrome import add_warning
from trac.util.translation import _
from formencode import Invalid, validators
from datetime import datetime
from trac.util.datefmt import utc, to_timestamp

name = 'testmanagement'
version = 1

NODE_TYPE_PROJECT   = (1 << 0)
NODE_TYPE_TESTSUITE = (1 << 1)
NODE_TYPE_TESTCASE  = (1 << 2)

FIELD_TYPE_STRING       = 1
FIELD_TYPE_TEXTAREA     = 2
FIELD_TYPE_CHECKBOX     = 3
FIELD_TYPE_LIST         = 4
FIELD_TYPE_MULTISELLIST = 5
FIELD_TYPE_DATE         = 6

FIELD_TYPES = (
    (FIELD_TYPE_STRING, 'String'),
    (FIELD_TYPE_TEXTAREA, 'Text Area'),
    (FIELD_TYPE_CHECKBOX, 'Checkbox'),
    (FIELD_TYPE_LIST, 'List'),
    (FIELD_TYPE_MULTISELLIST, 'Multiple Select List'),
    (FIELD_TYPE_DATE, 'Date'),
)


def get_field_type_name(type_id):
    for id, name in FIELD_TYPES:
        if type_id == id:
            return name
    return 'Unknown'


metadata = sa.MetaData()


node_table = sa.Table('tm_node', metadata,
    sa.Column('id', sa.Integer, primary_key=True, autoincrement=True),
    sa.Column('name', sa.Text, nullable=False),
    sa.Column('parent_id', None, sa.ForeignKey('tm_node.id')),
    sa.Column('node_type', sa.Integer),
    sa.Column('description', sa.Text),
    sa.Column('position', sa.Integer),
    sa.Column('active', sa.Integer),
    sa.Column('created', sa.Integer),
    sa.Column('modified', sa.Integer),
)

project_table = sa.Table('tm_project', metadata,
    sa.Column('node_id', None, sa.ForeignKey('tm_node.id'), primary_key=True),
)

testsuite_table = sa.Table('tm_testsuite', metadata,
    sa.Column('node_id', None, sa.ForeignKey('tm_node.id'), primary_key=True),
)

testcase_table = sa.Table('tm_testcase', metadata,
    sa.Column('node_id', None, sa.ForeignKey('tm_node.id'), primary_key=True),
    #sa.Column('id', sa.Integer, primary_key=True, autoincrement=True),
    #sa.Column('node_id', None, sa.ForeignKey('tm_node.id')),
    #sa.Column('version', sa.Integer, default=1),
    #sa.UniqueConstraint('id', 'version'),
)

checkitem_table = sa.Table('tm_checkitem', metadata,
    sa.Column('id', sa.Integer, primary_key=True, autoincrement=True),
    sa.Column('node_id', None, sa.ForeignKey('tm_node.id')),
    sa.Column('check_id', sa.Text),
    sa.Column('check_point', sa.Text),
)

customfield_table = sa.Table('tm_customfield', metadata,
    sa.Column('id', sa.Integer, primary_key=True, autoincrement=True),
    sa.Column('project_id', None, sa.ForeignKey('tm_node.id')),
    sa.Column('name', sa.Text, nullable=False),
    sa.Column('label', sa.Text, nullable=False),
    sa.Column('field_type', sa.Integer, nullable=False),
    sa.Column('possible_values', sa.Text),
    sa.Column('default_value', sa.Text),
    sa.Column('enable_on_project', sa.Boolean, default=False, nullable=False),
    sa.Column('enable_on_testsuite', sa.Boolean, default=False, nullable=False),
    sa.Column('enable_on_testcase', sa.Boolean, default=False, nullable=False),
    sa.Column('description', sa.Text),
)

cfieldvalue_table = sa.Table('tm_cfieldvalue', metadata,
    sa.Column('id', sa.Integer, primary_key=True, autoincrement=True),
    sa.Column('node_id', None, sa.ForeignKey('tm_node.id')),
    sa.Column('cfield_id', None, sa.ForeignKey('tm_customfield.id')),
    sa.Column('value', sa.Text),
)

#node_change_table = sa.Table('tm_node_change', metadata,
#    sa.Column('id', sa.Integer, primary_key=True, autoincrement=True),
#    sa.Column('node_id', None, sa.ForeignKey('tm_node.id')),
#    sa.Column('time', sa.Integer),
#    sa.Column('author', sa.Text),
#    sa.Column('field', sa.Text),
#    sa.Column('oldvalue', sa.Text),
#    sa.Column('newvalue', sa.Text),
#)


class TargetFieldMixin(object):
    def apply(self, args, default=None):
        for key, members in self.target_fields:
            if key in args:
                obj = self
                for member in members or '':
                    while hasattr(obj, member):
                        obj = getattr(obj, member)
                setattr(obj, key, args.get(key, default))

    @classmethod
    def build(cls, args, default=None, **extra_kw):
        kw = dict((x, args.get(x, default)) for x, y in cls.target_fields)
        kw.update(extra_kw)
        return cls(**kw)


class Node(object):
    parent = children = None
    def __init__(self, name, parent_id, node_type, description,
                 position=None, active=1, created=None, modified=None):
        self.name = name
        self.parent_id = parent_id
        self.node_type = node_type
        self.description = description
        self.position = position
        self.active = active
        self.created = created or to_timestamp(datetime.now(utc))
        self.modified = modified or to_timestamp(datetime.now(utc))

    def get_checkitem(self, checkitem_id):
        for checkitem in self.checkitems:
            if checkitem_id == checkitem.id:
                return checkitem
        return None

    def apply_checkitems(self, args, default=None):
        pass


class Project(TargetFieldMixin):
    target_fields = [
        ('name', ('node',)),
        ('description', ('node',)),
    ]

    def __init__(self, name, description, parent_id=None):
        self.node = Node(name, parent_id, NODE_TYPE_PROJECT, description)

    def validate(self, req):
        try:
            self.node.name = validators.UnicodeString(not_empty=True).to_python(self.node.name)
        except Invalid, e:
            add_warning(req, _('Project must contain a name.'))


class TestSuite(TargetFieldMixin):
    target_fields = [
        ('name', ('node',)),
        ('description', ('node',)),
    ]

    def __init__(self, name, description, parent_id=None):
        self.node = Node(name, parent_id, NODE_TYPE_TESTSUITE, description)

    def validate(self, req):
        try:
            self.node.name = validators.UnicodeString(not_empty=True).to_python(self.node.name)
        except Invalid, e:
            add_warning(req, _('Test suite must contain a name.'))
        try:
            self.node.parent_id = validators.Int().to_python(self.node.parent_id)
        except Invalid, e:
            add_warning(req, _('Invalid parameter.'))
        if self.node.id != None:
            try:
                self.node.id = validators.Int().to_python(self.node.id)
            except Invalid, e:
                add_warning(req, _('Invalid parameter.'))


class TestCase(TargetFieldMixin):
    target_fields = [
        ('name', ('node',)),
        ('description', ('node',)),
    ]

    def __init__(self, name, description, parent_id=None):
        self.node = Node(name, parent_id, NODE_TYPE_TESTCASE, description)

    def validate(self, req):
        try:
            self.node.name = validators.UnicodeString(not_empty=True).to_python(self.node.name)
        except Invalid, e:
            add_warning(req, _('Test case must contain a name.'))
        try:
            self.node.parent_id = validators.Int().to_python(self.node.parent_id)
        except Invalid, e:
            add_warning(req, _('Invalid parameter.'))
        if self.node.id != None:
            try:
                self.node.id = validators.Int().to_python(self.node.id)
            except Invalid, e:
                add_warning(req, _('Invalid parameter.'))


class CheckItem(object):
    id = node = None
    def __init__(self, node_id, check_id, check_point):
        self.node_id = node_id
        self.check_id = check_id
        self.check_point = check_point


class CustomField(TargetFieldMixin):
    target_fields = [
        ('name', None),
        ('label', None),
        ('field_type', None),
        ('possible_values', None),
        ('default_value', None),
        ('enable_on_project', None),
        ('enable_on_testsuite', None),
        ('enable_on_testcase', None),
        ('description', None),
    ]

    id = values = None
    def __init__(self, project_id, name, label, field_type,
                 enable_on_project=False, enable_on_testsuite=False,
                 enable_on_testcase=False,
                 description=None,
                 node_types=None, possible_values=None, default_value=None):
        self.project_id = project_id
        self.name = name
        self.label = label
        self.field_type = field_type
        self.possible_values = possible_values
        self.default_value = default_value
        self.enable_on_project = enable_on_project
        self.enable_on_testsuite = enable_on_testsuite
        self.enable_on_testcase = enable_on_testcase
        self.description = description
        if node_types != None:
            self.enable_on_project = (node_types & NODE_TYPE_PROJECT) != 0
            self.enable_on_testsuite = (node_types & NODE_TYPE_TESTSUITE) != 0
            self.enable_on_testcase = (node_types & NODE_TYPE_TESTCASE) != 0

    def validate(self, req):
        try:
            self.project_id = validators.Int().to_python(self.project_id)
        except Invalid, e:
            add_warning(req, _('Invalid project id.'))
        try:
            self.name = validators.UnicodeString(not_empty=True).to_python(self.name)
        except Invalid, e:
            add_warning(req, _('Custom field must contain a name.'))
        try:
            self.field_type = validators.Int().to_python(self.field_type)
        except Invalid, e:
            add_warning(req, _('Invalid field type.'))
        for key in ('project', 'testsuite', 'testcase'):
            key = 'enable_on_' + key
            try:
                setattr(self, key, validators.StringBool().to_python(getattr(self, key)))
            except Invalid, e:
                add_warning(req, _('Invalid value: %s=%s.' % (key, getattr(self, key))))

    def apply(self, args, default=None):
        for key in ('project', 'testsuite', 'testcase'):
            key = 'enable_on_' + key
            args[key] = args.get(key, 'False')
        super(CustomField, self).apply(args, default)


class CustomFieldValue(object):
    def __init__(self, node_id, cfield_id, value=None):
        self.node_id = node_id
        self.cfield_id = cfield_id
        self.value = value

    def validate(self, req):
        field_type = self.customfield.field_type
        try:
            if field_type in (FIELD_TYPE_STRING, FIELD_TYPE_TEXTAREA):
                self.value = validators.UnicodeString().to_python(self.value)
            elif field_type == FIELD_TYPE_CHECKBOX:
                if type(self.value) != bool:
                    self.value = validators.StringBoolean().to_python(self.value) and 'on' or 'off'
            elif field_type == FIELD_TYPE_LIST:
                self.value = validators.UnicodeString().to_python(self.value)
                # check if the value is subset of the possible value
                t = [x.strip() for x in self.customfield.possible_values.split(',')]
                t.append('')
                if self.value not in t:
                    raise Invalid(msg='Invalid selection', value=self.value, state=None)
            elif field_type == FIELD_TYPE_MULTISELLIST:
                if type(self.value) != list:
                    self.value = [self.value]
                s = set(self.value)
                t = set([x.strip() for x in self.customfield.possible_values.split(',')])
                if '' not in t:
                    t.add('')
                self.value = ', '.join(map(validators.UnicodeString().to_python, self.value))
                # check if the value is subset of the possible value
                if not s.issubset(t):
                    raise Invalid(msg='Invalid selection', value=self.value, state=None)
            elif field_type == FIELD_TYPE_DATE:
                # TODO
                pass
            else:
                msg = 'Unknown field type(%s)' % field_id
                raise Invalid(msg=msg, value=self.value, state=None)
        except Invalid, e:
            add_warning(req, _('%s: %s' % (e.msg, e.value)))


class NodeChange(object):
    pass


mapper(Node, node_table, properties=dict(
    children = relation(Node, lazy=True,
                        backref=backref('parent', remote_side=[node_table.c.id]),
                        cascade='all'),
    cfield_values = relation(CustomFieldValue, single_parent=True,
                             lazy=True, backref='node',
                             cascade='all, delete, delete-orphan'),
    checkitems = relation(CheckItem, lazy=False, backref='node',
                          cascade='all, delete, delete-orphan'),
))

mapper(Project, project_table, properties=dict(
    node = relation(Node, single_parent=True, lazy=False,
                    cascade='all, delete, delete-orphan'),
))

mapper(TestSuite, testsuite_table, properties=dict(
    node = relation(Node, single_parent=True, lazy=False,
                    cascade='all, delete, delete-orphan'),
))

mapper(TestCase, testcase_table, properties=dict(
    node = relation(Node, single_parent=True, lazy=False,
                    cascade='all, delete, delete-orphan'),
))

mapper(CheckItem, checkitem_table, properties=dict(
))

mapper(CustomField, customfield_table, properties=dict(
    values = relation(CustomFieldValue, lazy=True, backref='customfield',
                      cascade='all'),
))

mapper(CustomFieldValue, cfieldvalue_table, properties=dict(
))

#mapper(NodeChange, node_change_table, properties=dict(
#))


__all__ = [
    'Node',
    'Project',
    'TestSuite',
    'TestCase',
    'CheckItem',
    'CustomField',
    'CustomFieldValue',
    'NODE_TYPE_PROJECT',
    'NODE_TYPE_TESTSUITE',
    'NODE_TYPE_TESTCASE',
    'FIELD_TYPE_STRING',
    'FIELD_TYPE_TEXTAREA',
    'FIELD_TYPE_CHECKBOX',
    'FIELD_TYPE_LIST',
    'FIELD_TYPE_MULTISELLIST',
    'FIELD_TYPE_DATE',
    'FIELD_TYPES',
    'get_field_type_name',
]

